#include <stdio.h>
#include <time.h>
#include <string.h>
#include <ctype.h>

#include "midifile.h"

// generic MIDI file playing functions

#define MAXTRACKS             64
#define MIDI_MThd             0x4d546864
#define MIDI_MTrk             0x4d54726b

#define WRAPAROUNDTIME        1000000000


typedef struct miditrack {
  char *start, *ptr, *end;
  char running_status;
  int next_timecode;
  int initialno;
} miditrack;

static miditrack tracks[MAXTRACKS];
static int timeoflastevent;
static int trackcount = 0;
static int pausestarttime, paused = 0;
static int ticksperquarter;
static int uspertick = 384;
static int usperquarter = 300000;
static int timeoffset;                  // milliseconds


static int readticks(char **tp);
static int read4(char **tp);
static int read3(char **tp);
static int read2(char **tp);
static int quick_play(void);
static int pre_parse_tracks(char *midi, int tcount);



// stop playback
void midifile_stop() {

  trackcount = 0;
}


// pause/restart playback
// on entry
// pause            1 to pause, != 1 to unpause
// timefrozen       1 if MIDI-clock was stopped during pause
int midifile_pause(int pause, int timefrozen) {

  if (pause) {
    if (!paused)  pausestarttime = clock();
    paused = 1;

  } else {
    if (paused)
      if (!timefrozen)
        timeoffset += (clock()-pausestarttime)*10;
    paused = 0;
  }

  return timeoffset;
}


// parse file, start playback
// on entry
// midiptr          pointer to midi-file in memory
// midisize         size of midi-file
// totalduration    returns the duration in milliseconds
// returns MIDI_FAILED or MIDI_PLAYING
int midifile_start(char *midiptr, int midisize, int *totalduration) {

  int id, len, format, tcount;
  char *midi;

  usperquarter = 300000;
  uspertick = 384;

  midi = midiptr;
  id = read4(&midi);
  len = read4(&midi);
  format = read2(&midi);
  trackcount = read2(&midi);
  ticksperquarter = read2(&midi);
  if (ticksperquarter & (1<<15)) {
    fprintf(stderr, "MTC not supported\n");
    return MIDI_FAILED;
  }

  if ((id != MIDI_MThd) || (format > 1)) {
    fprintf(stderr, "Unsupported MIDIfile format\n");
    return MIDI_FAILED;
  }

  if (trackcount > MAXTRACKS)  trackcount = MAXTRACKS;

  // read track pointers and reset track info
  tcount = trackcount;
  if (pre_parse_tracks(midi, trackcount))   return MIDI_FAILED;

  // scan the tracks to find duration
  timeoflastevent = 0;
  timeoffset = 0;
  paused = 0;
  *totalduration = quick_play();
  if (*totalduration <= 0)   return MIDI_FAILED;

  // reread the track info and pointers (invalidated by quick_play())
  trackcount = tcount;
  pre_parse_tracks(midi, trackcount);
  timeoflastevent = 0;
  timeoffset = 0;             // milliseconds
  paused = 0;

  return MIDI_PLAYING;
}


// poll playback, return information
// on entry
// position         returns current position in centiseconds from start
// playevent        function to call to play event
//                  arg0: first part of the event
//                  arg1: event time in milliseconds
//                  should return 0 if it cannot accept new event immediately
// available        no. of events the scheduler-buffer has room left for
// ahead            diff between first/last event in the scheduler-buffer, in milliseconds
// returns MIDI_PLAYING or MIDI_PAUSED or MIDI_FINISHED or MIDI_FAILED
int midifile_poll(int *position, int(playevent)(int event, int ms), int available, int maxtime) {

  int freespace;

  if (paused)                 return MIDI_PAUSED;
  if (trackcount == 0) {
    fprintf(stderr, "All tracks have finished\n");
    return MIDI_FINISHED;
  }
  if (available <= 2)         return MIDI_PLAYING;

  if (available > 100)
    freespace = 100;          // fill max 100 events
  else
    freespace = available;

  do {
    int track, nexttrack, closesttime, millisecond;

    nexttrack = -1;
    closesttime = 0x7f000000;
    for (track = 0; track < trackcount; track++) {
      if (tracks[track].next_timecode - timeoflastevent < closesttime) {
        nexttrack = track;
        closesttime = tracks[nexttrack].next_timecode - timeoflastevent;
      }
    }
    if (nexttrack == -1) {
      fprintf(stderr, "Failed to find an event...\n");
      return MIDI_FAILED;
    }

    // get next event
    if ((tracks[nexttrack].ptr[0] & 0xf0) == SYSTEM) {
      switch (tracks[nexttrack].ptr[0] & 0x0f) {
      case SYSTEM_EXCL1:
      case SYSTEM_EXCL2:
        {
          int len;
          fprintf(stderr, "SYSEX found at offset %d in track %d - may cause problems\n",
                           tracks[nexttrack].ptr - tracks[nexttrack].start,
                           tracks[nexttrack].initialno);
          tracks[nexttrack].ptr++;      // skip status
          len = readticks(&tracks[nexttrack].ptr);
          tracks[nexttrack].ptr += len;
        }
        break;
      case SYSTEM_META:
        {
          int meta, len;

          tracks[nexttrack].ptr++;      // skip status
          meta = *tracks[nexttrack].ptr++;
          len = readticks(&tracks[nexttrack].ptr);
          switch (meta) {
          case META_SETTEMPO:
            {
              char *p;
              p = tracks[nexttrack].ptr;
              usperquarter = read3(&p);
              uspertick = usperquarter/ticksperquarter;
            }
            break;
          }
          tracks[nexttrack].ptr += len;
        }
        break;
      }

    } else {
      int event, status;

      // get status byte
      status = tracks[nexttrack].ptr[0];
      if (status & 0x80) {
        tracks[nexttrack].running_status = status;
        tracks[nexttrack].ptr++;
      } else
        status = tracks[nexttrack].running_status;

      event = 0x00000002;                         // length
      event |= status<<8;
      event |= (*tracks[nexttrack].ptr++)<<16;
      status &= 0xf0;
      if (!((status == PROGRAMCHANGE) || (status == CHANNELPRESSURE))) {
        event |= (*tracks[nexttrack].ptr++)<<24;
        event = (event & 0xffffff00) | 3;         // set length
      }

      millisecond = tracks[nexttrack].next_timecode/1000+timeoffset;
      freespace--;
      if (playevent(event, millisecond) <= 0)   freespace = 0;
      if (millisecond >= maxtime)               freespace = 0;
    }
    // get time stamp for next event
    timeoflastevent = tracks[nexttrack].next_timecode;
    tracks[nexttrack].next_timecode += readticks(&tracks[nexttrack].ptr)*uspertick;
    // 32 bits is not enough when using microseconds
    if (timeoflastevent > WRAPAROUNDTIME) {
      int track;

      timeoflastevent -= WRAPAROUNDTIME;
      timeoffset += WRAPAROUNDTIME/1000;
      for (track = 0; track < trackcount; track++)
        tracks[nexttrack].next_timecode -= WRAPAROUNDTIME;
    }

    if (tracks[nexttrack].ptr >= tracks[nexttrack].end) {
      // at end of track, simply remove the track from the list
      if (nexttrack < trackcount - 1)
        memcpy(tracks+nexttrack, tracks+trackcount-1, sizeof(miditrack));
      trackcount--;
      if (trackcount == 0)    return MIDI_FINISHED;
    }

  } while (freespace > 0);

  *position = timeoflastevent/1000 + timeoffset;

  return MIDI_PLAYING;
}

// ----------------------------------------------------------------------------

// various midi functions
int readticks(char **tp) {

  int byte, n;
  char *t;

  t = *tp;

  byte = *t++;
  n = byte & 0x7f;
  if (byte & 0x80) {
    byte = *t++;
    n = (n << 7) | (byte & 0x7f);
    if (byte & 0x80) {
      byte = *t++;
      n = (n << 7) | (byte & 0x7f);
      if (byte & 0x80) {
        byte = *t++;
        n = (n << 7) | (byte & 0x7f);
      }
    }
  }

  *tp = t;

  return n;
}


int read4(char **tp) {

  int word = 0;
  char *t;

  t = *tp;

  word = *t++;
  word = (word << 8) | *t++;
  word = (word << 8) | *t++;
  word = (word << 8) | *t++;
  *tp = t;

  return word;
}

int read3(char **tp) {

  int word = 0;
  char *t;

  t = *tp;

  word = *t++;
  word = (word << 8) | *t++;
  word = (word << 8) | *t++;
  *tp = t;

  return word;
}

int read2(char **tp) {

  int word = 0;
  char *t;

  t = *tp;

  word = *t++;
  word = (word << 8) | *t++;
  *tp = t;

  return word;
}


// ----------------------------------------------------------------------------
//
int quick_play() {

  int millisecond;

  millisecond = 0;

  do {
    int track, nexttrack, closesttime;

    nexttrack = -1;
    closesttime = 0x7f000000;
    for (track = 0; track < trackcount; track++) {
      if (tracks[track].next_timecode - timeoflastevent < closesttime) {
        nexttrack = track;
        closesttime = tracks[nexttrack].next_timecode - timeoflastevent;
      }
    }
    if (nexttrack == -1)      return -1;

    // get next event
    if ((tracks[nexttrack].ptr[0] & 0xf0) == SYSTEM) {
      switch (tracks[nexttrack].ptr[0] & 0x0f) {
      case SYSTEM_EXCL1:
      case SYSTEM_EXCL2:
        {
          int len;
          tracks[nexttrack].ptr++;      // skip status
          len = readticks(&tracks[nexttrack].ptr);
          tracks[nexttrack].ptr += len;
        }
        break;
      case SYSTEM_META:
        {
          int meta, len;

          tracks[nexttrack].ptr++;      // skip status
          meta = *tracks[nexttrack].ptr++;
          len = readticks(&tracks[nexttrack].ptr);
          switch (meta) {
          case META_SETTEMPO:
            {
              char *p;
              p = tracks[nexttrack].ptr;
              usperquarter = read3(&p);
              uspertick = usperquarter/ticksperquarter;
            }
            break;
          }
          tracks[nexttrack].ptr += len;
        }
        break;
      }

    } else {
      int status;

      // get status byte
      status = tracks[nexttrack].ptr[0];
      if (status & 0x80) {
        tracks[nexttrack].running_status = status;
        tracks[nexttrack].ptr++;
      } else
        status = tracks[nexttrack].running_status;

      tracks[nexttrack].ptr++;
      status &= 0xf0;
      if (!((status == PROGRAMCHANGE) || (status == CHANNELPRESSURE)))
        tracks[nexttrack].ptr++;

      millisecond = tracks[nexttrack].next_timecode/1000+timeoffset;
    }

    // get time stamp for next event
    timeoflastevent = tracks[nexttrack].next_timecode;
    tracks[nexttrack].next_timecode += readticks(&tracks[nexttrack].ptr)*uspertick;
    // 32 bits is not enough when using microseconds
    if (timeoflastevent > WRAPAROUNDTIME) {
      int track;

      timeoflastevent -= WRAPAROUNDTIME;
      timeoffset += WRAPAROUNDTIME/1000;
      for (track = 0; track < trackcount; track++)
        tracks[nexttrack].next_timecode -= WRAPAROUNDTIME;
    }

    if (tracks[nexttrack].ptr >= tracks[nexttrack].end) {
      // at end of track, simply remove the track from the list
      if (nexttrack < trackcount - 1)
        memcpy(tracks+nexttrack, tracks+trackcount-1, sizeof(miditrack));
      trackcount--;
      if (trackcount == 0)    return millisecond;
    }

  } while (1);

  return 0;
}



int pre_parse_tracks(char *midi, int tcount) {

  int track;

  for (track = 0; track < tcount; track++) {
    int id, len;

    id = read4(&midi);
    if (id != MIDI_MTrk) {
      fprintf(stderr, "Bad trackheader\n");
      return 1;
    }
    len = read4(&midi);
    tracks[track].start = midi;
    tracks[track].ptr = midi;
    tracks[track].end = midi + len;
    tracks[track].initialno = track;
    tracks[track].running_status = 0;
    tracks[track].next_timecode = readticks(&tracks[track].ptr);
    midi = tracks[track].end;
  }

  return 0;
}
